{
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
}

unit MatchQuestionEditor;

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ComCtrls,
  ExtCtrls, ActnList, Shape, VisualUtils, PadEditor, superobject,
  MiscUtils, Spacer, MatchQuestionOptions, QuestionEditors, Questions,
  MatchQuestion;

type
  TMatchQuestionEditorFrame = class(TShapeFrame, IQuestionEditor)
    actAddPair: TAction;
    actAddDistractor: TAction;
    actDeleteRow: TAction;
    actSetOptions: TAction;
    actMoveRowDown: TAction;
    actMoveRowUp: TAction;
    alQuestion: TActionList;
    ilQuestion: TImageList;
    pnlFormulation: TPanel;
    pnlItems: TPanel;
    pnlQuestion: TPanel;
    sbxQuestion: TScrollBox;
    tbrQuestion: TToolBar;
    tbAddPair: TToolButton;
    tbAddDistractor: TToolButton;
    tbDeleteRow: TToolButton;
    tbSeparator1: TToolButton;
    tbMoveRowUp: TToolButton;
    tbMoveRowDown: TToolButton;
    tbSeparator2: TToolButton;
    tbSetOptions: TToolButton;
    procedure actAddDistractorExecute(Sender: TObject);
    procedure actAddDistractorUpdate(Sender: TObject);
    procedure actAddPairExecute(Sender: TObject);
    procedure actAddPairUpdate(Sender: TObject);
    procedure actDeleteRowExecute(Sender: TObject);
    procedure actDeleteRowUpdate(Sender: TObject);
    procedure actMoveRowDownExecute(Sender: TObject);
    procedure actMoveRowDownUpdate(Sender: TObject);
    procedure actMoveRowUpExecute(Sender: TObject);
    procedure actMoveRowUpUpdate(Sender: TObject);
    procedure actSetOptionsExecute(Sender: TObject);
  private
    FFormulation: TPadEditorFrame;
    FLeftEditors: TPadEditorFrameList;
    FRightEditors: TPadEditorFrameList;
    FOldLeftEditorIds: UnicodeString;
    FOldRightEditorIds: UnicodeString;
    FMaxRows: Integer;
    FSelectRow: Integer;
    FSelectRightItem: Boolean;
    FOptionsImageIndex: Integer;
    FCustomOptionsImageIndex: Integer;
    procedure SyncItemEditors;
    procedure GetSelectedItem(out RowIndex: Integer; out Right: Boolean);
    function SelectedRowIndex: Integer;
    procedure MoveRow(Down: Boolean);
    function CanMoveRowBy(Offset: Integer): Boolean;
    function IsDistractorRow(Index: Integer): Boolean;
    { private declarations }
  public
    procedure AfterConstruction; override;
    destructor Destroy; override;
    procedure Render;
    { public declarations }
  end; 

implementation

{$R *.lfm}
{$R *.rc}

resourcestring
  SMatchQuestion = 'Matching Question';
  SInitialText = 'Match the items.';

{ TMatchQuestionEditorFrame }

procedure TMatchQuestionEditorFrame.actAddPairExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'addPair');
  FSelectRow := FLeftEditors.Count;
  FSelectRightItem := FALSE;
end;

procedure TMatchQuestionEditorFrame.actAddPairUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := FRightEditors.Count < FMaxRows;
end;

procedure TMatchQuestionEditorFrame.actDeleteRowExecute(Sender: TObject);
var
  RowIndex: Integer;
  Right: Boolean;
  Input: ISuperObject;
begin
  GetSelectedItem(RowIndex, Right);

  Input := SO;
  Input.I['index'] := RowIndex;
  Dispatcher.PerformAction(Id, 'deleteRow', Input);

  if not Right and (RowIndex = FLeftEditors.Count-1) and (FRightEditors.Count > FLeftEditors.Count) then
    FSelectRightItem := TRUE
  else
    FSelectRightItem := Right;

  if RowIndex = FRightEditors.Count-1 then
    FSelectRow := RowIndex-1
  else
    FSelectRow := RowIndex;
end;

procedure TMatchQuestionEditorFrame.actDeleteRowUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := SelectedRowIndex <> -1;
end;

procedure TMatchQuestionEditorFrame.actMoveRowDownExecute(Sender: TObject);
begin
  MoveRow(TRUE);
end;

procedure TMatchQuestionEditorFrame.actMoveRowDownUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := CanMoveRowBy(1);
end;

procedure TMatchQuestionEditorFrame.actMoveRowUpExecute(Sender: TObject);
begin
  MoveRow(FALSE);
end;

procedure TMatchQuestionEditorFrame.actMoveRowUpUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := CanMoveRowBy(-1);
end;

procedure TMatchQuestionEditorFrame.actSetOptionsExecute(Sender: TObject);
begin
  TMatchQuestionOptionsForm.OpenModal(Id, Shape.Clone, Dispatcher);
end;

function TMatchQuestionEditorFrame.CanMoveRowBy(Offset: Integer): Boolean;
var
  Index, NewIndex: Integer;
begin
  Index := SelectedRowIndex;
  NewIndex := Index + Offset;
  Result := (Index <> -1) and (NewIndex >= 0) and (NewIndex < FRightEditors.Count)
    and (
      (IsDistractorRow(Index) and IsDistractorRow(NewIndex))
      or
      (not IsDistractorRow(Index) and not IsDistractorRow(NewIndex))
    );
end;

function TMatchQuestionEditorFrame.IsDistractorRow(Index: Integer): Boolean;
begin
  Result := Index >= FLeftEditors.Count;
end;

procedure TMatchQuestionEditorFrame.actAddDistractorExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'addDistractor');
  FSelectRow := FRightEditors.Count;
  FSelectRightItem := TRUE;
end;

procedure TMatchQuestionEditorFrame.actAddDistractorUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := FRightEditors.Count < FMaxRows;
end;

procedure TMatchQuestionEditorFrame.SyncItemEditors;
var
  LeftIds, RightIds: ISuperObject;
  LeftChanged, RightChanged: Boolean;
  i: Integer;
begin
  LeftIds := Shape['leftEditor'];
  RightIds := Shape['rightEditor'];
  LeftChanged := AssignDiffering(LeftIds.AsJSon, FOldLeftEditorIds);
  RightChanged := AssignDiffering(RightIds.AsJSon, FOldRightEditorIds);
  if LeftChanged or RightChanged then
  begin
    FLeftEditors.Clear;
    FRightEditors.Clear;
    DestroyOwnedComponents(pnlItems);
    for i := 0 to RightIds.AsArray.Length-1 do
    begin
      if i < LeftIds.AsArray.Length then
        FLeftEditors.AddSafely(TPadEditorFrame.Create(LeftIds.AsArray.S[i], Dispatcher, pnlItems))
      else
        TSpacer.Create(pnlItems).Parent := pnlItems;
      FRightEditors.AddSafely(TPadEditorFrame.Create(RightIds.AsArray.S[i], Dispatcher, pnlItems));
    end;
  end;
end;

procedure TMatchQuestionEditorFrame.GetSelectedItem(out RowIndex: Integer; out Right: Boolean);
var
  i: Integer;
begin
  Right := TRUE;
  for i := 0 to FRightEditors.Count-1 do
    if ContainsActiveFormControl(FRightEditors[i]) then
    begin
      RowIndex := i;
      Exit;
    end;
  Right := FALSE;
  for i := 0 to FLeftEditors.Count-1 do
    if ContainsActiveFormControl(FLeftEditors[i]) then
    begin
      RowIndex := i;
      Exit;
    end;
  RowIndex := -1;
end;

function TMatchQuestionEditorFrame.SelectedRowIndex: Integer;
var
  Dummy: Boolean;
begin
  GetSelectedItem(Result, Dummy);
end;

procedure TMatchQuestionEditorFrame.MoveRow(Down: Boolean);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.I['index'] := SelectedRowIndex;
  Input.B['forward'] := Down;
  Dispatcher.PerformAction(Id, 'moveRow', Input);
end;

procedure TMatchQuestionEditorFrame.AfterConstruction;
begin
  inherited;
  alQuestion.OnExecute := Scene.ActionListExecuteFilter;
  LoadActionImages(alQuestion);
  MakeVerticallyAutoScrollable(sbxQuestion);

  FFormulation := TPadEditorFrame.Create(Shape.S['formulationEditor'], Dispatcher, pnlFormulation);
  FFormulation.Align := alClient;
  FFormulation.Border := TRUE;

  FLeftEditors := TPadEditorFrameList.Create;
  FRightEditors := TPadEditorFrameList.Create;
  FMaxRows := Shape.I['maxRows'];
  FSelectRow := -1;

  FOptionsImageIndex := AddPngResourceToImageList('MatchQuestionEditor/options.png', ilQuestion);
  FCustomOptionsImageIndex := AddPngResourceToImageList('MatchQuestionEditor/customOptions.png', ilQuestion);
end;

destructor TMatchQuestionEditorFrame.Destroy;
begin
  FreeAndNil(FFormulation);
  FreeAndNil(FLeftEditors);
  FreeAndNil(FRightEditors);
  inherited;
end;

procedure TMatchQuestionEditorFrame.Render;
var
  Editor: TPadEditorFrame;
begin
  UpdateShape;

  DisableAutoSizing;
  try
    FFormulation.Render;
    SyncItemEditors;
    for Editor in FLeftEditors do
      Editor.Render;
    for Editor in FRightEditors do
      Editor.Render;
  finally
    EnableAutoSizing;
  end;

  if FSelectRow >= 0 then
  begin
    if FSelectRightItem and (FSelectRow < FRightEditors.Count) then
      FRightEditors[FSelectRow].SelectFirst
    else if not FSelectRightItem and (FSelectRow < FLeftEditors.Count) then
      FLeftEditors[FSelectRow].SelectFirst;
  end;
  FSelectRow := -1;
  FSelectRightItem := FALSE;

  if Shape.B['hasDefaultOptions'] then
    actSetOptions.ImageIndex := FOptionsImageIndex
  else
    actSetOptions.ImageIndex := FCustomOptionsImageIndex;
end;

initialization

  QuestionRegistry.Add(TMatchQuestion.Kind, SMatchQuestion, 'MatchQuestionEditor/matchQuestion.png', SInitialText);
  QuestionEditorRegistry.Add(TMatchQuestionEditorFrame, $f3c03980638755a2);

end.

